iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
Software Development

Android 鐵人賽: 天命最高 - 陪伴大家一步步打造屬於自己的app系列 第 11

Android Widget 實戰——計數器、圖片切換與共用按鈕事件

  • 分享至 

  • xImage
  •  

Day 11:Android UI 介面: 實戰——計數器、圖片切換與共用按鈕事件

昨天學習了 Android 的版面配置、View Binding,以及 Activity 與 Fragment 的基本操作。今天要繼續認識 Android 常用的介面元件(Widget),並透過四個範例練習 TextViewButtonImageViewImageButton 的互動方式。

今天會完成:

  • 使用 View Binding 操作按鈕與文字。
  • 製作可以增加、減少及歸零的計數器。
  • 使用 when 判斷是哪一個按鈕被點擊。
  • 使用 ImageView 動態切換圖片。
  • 使用 ImageButton 建立圖片按鈕。
  • 讓多個按鈕共用同一個事件監聽器。

參考資料:Android Developers-View BindingAndroid Developers-Button


一、什麼是 Android Widget?

Android Widget 是放在 App 畫面上的基本介面元件,例如:

Widget 用途
TextView 顯示文字或數字
Button 一般文字按鈕
ImageView 顯示圖片
ImageButton 可以被點擊的圖片按鈕
EditText 讓使用者輸入文字
CheckBox 提供多選項目
RadioButton 提供單選項目
Switch 切換開啟或關閉狀態

我們可以在 XML 中決定元件的外觀與位置,再由 Kotlin 程式設定點擊事件。當使用者按下按鈕時,Android 便會呼叫對應的 OnClickListener

參考資料:Android Developers-使用 Views 建構版面Android Developers-輸入事件


範例一:使用 View Binding 操作三個按鈕

第一個範例延續昨天的 View Binding。畫面中有三個 Button 與三個 TextView,按下不同按鈕後,修改對應的文字。

MainActivity(4).kt 完整程式

package com.example.layout3

import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.layout3.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    // 宣告 View Binding。
    // lateinit 表示稍後會在 onCreate() 中完成初始化。
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 啟用 Edge-to-Edge,讓 App 內容可以延伸到系統列區域。
        enableEdgeToEdge()

        // 根據 activity_main.xml 建立 Binding 物件。
        binding = ActivityMainBinding.inflate(layoutInflater)

        // 將 XML 的根 View 設定為 Activity 畫面。
        setContentView(binding.root)

        // 取得狀態列及導覽列的 Insets,
        // 並將其設為根版面的 Padding,避免內容遭系統列遮住。
        ViewCompat.setOnApplyWindowInsetsListener(binding.main) { view, insets ->

            val systemBars =
                insets.getInsets(WindowInsetsCompat.Type.systemBars())

            view.setPadding(
                systemBars.left,
                systemBars.top,
                systemBars.right,
                systemBars.bottom
            )

            insets
        }

        // 按下第一個按鈕,修改第一個 TextView。
        binding.button1.setOnClickListener {
            binding.textViewText1.text = "Button-1 Clicked"
        }

        // 按下第二個按鈕,修改第二個 TextView。
        binding.button2.setOnClickListener {
            binding.textViewText2.text = "Button-2 Clicked"
        }

        // 按下第三個按鈕,修改第三個 TextView。
        binding.button3.setOnClickListener {
            binding.textViewText3.text = "Button-3 Clicked"
        }
    }
}

View Binding 會根據 XML 檔名自動產生 Binding 類別。例如 activity_main.xml 會產生 ActivityMainBinding,XML 中的 button1 則可以透過 binding.button1 直接存取。

參考資料:Android Developers-在 Activity 使用 View Binding


setOnClickListener 的 Lambda 寫法

範例使用以下方式處理按鈕點擊:

binding.button1.setOnClickListener {
    binding.textViewText1.text = "Button-1 Clicked"
}

setOnClickListener 需要接收一個 View.OnClickListener。因為 OnClickListener 只有一個抽象函式,所以 Kotlin 可以利用 SAM 轉換,將完整的匿名物件簡化成 Lambda。

完整匿名物件寫法如下:

binding.button1.setOnClickListener(
    object : android.view.View.OnClickListener {

        override fun onClick(view: android.view.View?) {
            binding.textViewText1.text = "Button-1 Clicked"
        }
    }
)

Lambda 寫法可以減少重複的樣板程式碼,使按鈕行為更容易閱讀。

參考資料:Android Developers-回應按鈕點擊事件


範例二:製作加減計數器

第二個範例要製作一個簡單的計數器,畫面中包含:

  • 顯示目前數值的 TextView
  • 增加數值的 button_plus
  • 減少數值的 button_minus
  • 將數值歸零的 button_reset

這次不為每個按鈕分別建立 Lambda,而是讓三個按鈕共用同一個 MyButton 事件監聽器。

參考資料:Android Developers-View.OnClickListener

MainActivity(5).kt 完整程式

package com.example.widget_2

import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat

class MainActivity : AppCompatActivity() {

    // 宣告三個功能按鈕。
    private lateinit var buttonReset: Button
    private lateinit var buttonMinus: Button
    private lateinit var buttonPlus: Button

    // 顯示目前計數結果的 TextView。
    private lateinit var textViewCounter: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 啟用 Edge-to-Edge 顯示。
        enableEdgeToEdge()

        // 載入 activity_main.xml。
        setContentView(R.layout.activity_main)

        // 處理系統狀態列與導覽列的範圍。
        ViewCompat.setOnApplyWindowInsetsListener(
            findViewById(R.id.main)
        ) { view, insets ->

            val systemBars =
                insets.getInsets(WindowInsetsCompat.Type.systemBars())

            view.setPadding(
                systemBars.left,
                systemBars.top,
                systemBars.right,
                systemBars.bottom
            )

            insets
        }

        // 使用 findViewById() 取得 XML 中的畫面元件。
        textViewCounter = findViewById(R.id.txt_CountValue)
        buttonPlus = findViewById(R.id.button_plus)
        buttonMinus = findViewById(R.id.button_minus)
        buttonReset = findViewById(R.id.button_reset)

        // 三個按鈕共用 MyButton 事件監聽器。
        buttonPlus.setOnClickListener(MyButton())
        buttonMinus.setOnClickListener(MyButton())
        buttonReset.setOnClickListener(MyButton())
    }

    /**
     * 共用的按鈕事件監聽器。
     *
     * 使用 inner 宣告內部類別後,MyButton 才能直接存取
     * MainActivity 中的 textViewCounter 等成員。
     */
    inner class MyButton : View.OnClickListener {

        override fun onClick(view: View?) {

            // 透過被點擊 View 的 ID,判斷使用者按了哪個按鈕。
            when (view?.id) {

                R.id.button_plus -> {

                    // 先將 TextView 顯示的文字轉成 String。
                    val data = textViewCounter.text.toString()

                    // 再將 String 轉成 Int,才能執行加法。
                    var number = data.toInt()

                    // 數值加一。
                    number++

                    // TextView.text 需要文字,因此再將 Int 轉回 String。
                    textViewCounter.text = number.toString()
                }

                R.id.button_minus -> {

                    // 讀取畫面目前顯示的數值。
                    val data = textViewCounter.text.toString()
                    var number = data.toInt()

                    // 只有大於零時才減一,避免出現負數。
                    if (number > 0) {
                        number--
                    }

                    // 將計算結果顯示回 TextView。
                    textViewCounter.text = number.toString()
                }

                R.id.button_reset -> {

                    // 按下 Reset 時,直接將數值設為零。
                    val number = 0
                    textViewCounter.text = number.toString()
                }
            }
        }
    }
}

參考資料:Android Developers-處理輸入事件


計數器的資料轉換流程

TextView.text 的型別是 CharSequence,不能直接執行整數加法。因此計數器需要完成以下轉換:

val data = textViewCounter.text.toString()
var number = data.toInt()

number++

textViewCounter.text = number.toString()

轉換流程為:

TextView.text
→ CharSequence
→ String
→ Int
→ 執行數學運算
→ String
→ 顯示回 TextView

如果畫面中的文字不一定是有效數字,可以改用 toIntOrNull(),避免格式錯誤造成 App 閃退:

val number = textViewCounter.text
    .toString()
    .toIntOrNull()
    ?: 0

當轉換失敗時,toIntOrNull() 會回傳 null,而 Elvis 運算子 ?: 會讓程式改用預設值 0

參考資料:Kotlin-字串轉數字


為什麼要使用 inner class?

在 Kotlin 中,巢狀類別預設無法直接存取外部類別的成員。

如果把 MyButton 寫成:

class MyButton : View.OnClickListener

它便不能直接使用 MainActivity 裡的 textViewCounter

加上 inner 後:

inner class MyButton : View.OnClickListener

MyButton 物件會持有外部 MainActivity 的參考,因此可以操作 Activity 的畫面元件。

參考資料:Kotlin-Nested and inner classes


使用 when 判斷按鈕 ID

三個按鈕使用相同監聽器,所以必須知道實際被點擊的是哪一個 View。

onClick() 參數中的 view 就是被點擊的元件,因此可以讀取:

view?.id

再交給 when 判斷:

when (view?.id) {

    R.id.button_plus -> {
        // 執行增加
    }

    R.id.button_minus -> {
        // 執行減少
    }

    R.id.button_reset -> {
        // 執行歸零
    }
}

這種寫法適合多個按鈕行為相似,或需要統一管理事件的情況。

參考資料:Kotlin-when 條件運算式


範例三:使用按鈕切換 ImageView 圖片

第三個範例使用一個 ImageView 顯示圖片,並提供三個按鈕。按下按鈕後,除了切換圖片,也會修改圖片說明文字。

圖片應先放進:

app/src/main/res/drawable/

例如:

drawable/img1.jpg
drawable/img2.jpg
drawable/img3.jpg

圖片資源放入 drawable 後,Android 會在 R.drawable 中產生對應的資源 ID。

參考資料:Android Developers-Drawable 資源

MainActivity(6).kt 完整程式

package com.example.widget_3

import android.os.Bundle
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat

class MainActivity : AppCompatActivity() {

    // 顯示目前圖片說明文字。
    private lateinit var textView: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 啟用 Edge-to-Edge。
        enableEdgeToEdge()

        // 載入 activity_main.xml。
        setContentView(R.layout.activity_main)

        // 避免畫面被狀態列或導覽列遮住。
        ViewCompat.setOnApplyWindowInsetsListener(
            findViewById(R.id.main)
        ) { view, insets ->

            val systemBars =
                insets.getInsets(WindowInsetsCompat.Type.systemBars())

            view.setPadding(
                systemBars.left,
                systemBars.top,
                systemBars.right,
                systemBars.bottom
            )

            insets
        }

        // 取得負責顯示圖片的 ImageView。
        val imageView =
            findViewById<ImageView>(R.id.imageView)

        // 取得三個切換圖片的按鈕。
        val button1 =
            findViewById<Button>(R.id.button)

        val button2 =
            findViewById<Button>(R.id.button2)

        val button3 =
            findViewById<Button>(R.id.button3)

        // 取得顯示圖片說明的 TextView。
        textView =
            findViewById(R.id.textView)

        // 按下第一個按鈕,顯示 img1 並修改說明文字。
        button1.setOnClickListener {

            imageView.setImageResource(
                R.drawable.img1
            )

            textView.text = "This is a flower 1"
        }

        // 按下第二個按鈕,顯示 img2 並修改說明文字。
        button2.setOnClickListener {

            imageView.setImageResource(
                R.drawable.img2
            )

            textView.text = "This is a flower 2"
        }

        // 按下第三個按鈕,顯示 img3 並修改說明文字。
        button3.setOnClickListener {

            imageView.setImageResource(
                R.drawable.img3
            )

            textView.text = "This is a flower 3"
        }
    }
}

參考資料:Android Developers-ImageView


使用 setImageResource() 切換圖片

ImageView.setImageResource() 可以把指定的 Drawable 資源設定成 ImageView 的顯示內容。

語法如下:

imageView.setImageResource(R.drawable.img1)

其中:

  • imageView:準備更新的圖片元件。
  • setImageResource():設定圖片資源的方法。
  • R.drawable.img1:放在 drawable 目錄中的圖片資源 ID。

如果要清除目前圖片,可以傳入 0

imageView.setImageResource(0)

setImageResource() 適合切換 App 內建的小型圖片;若要讀取網路圖片或大型照片,通常會使用 Coil、Glide 等圖片載入函式庫,避免在主執行緒進行過多圖片解碼工作。

參考資料:Android Developers-ImageView.setImageResource


範例四:ImageButton 與共用事件監聽器

第四個範例把一般 Button 改為 ImageButton。畫面中共有六個圖片按鈕與三個 TextView,每兩個圖片按鈕控制一個文字區域。

例如:

ImageButton 修改的 TextView 顯示內容
imageButton1 tv1 this is flower 1
imageButton2 tv2 this is flower 2
imageButton3 tv3 this is flower 3
imageButton4 tv1 this is flower 4
imageButton5 tv2 this is flower 5
imageButton6 tv3 this is flower 6

這個範例同時使用 View Binding 與共用的 MyImgButton 事件監聽器。

參考資料:Android Developers-ImageButtonAndroid Developers-View Binding

MainActivity(7).kt 完整程式

package com.example.widget_41f

import android.os.Bundle
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.widget_41f.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    // Activity 對應的 View Binding。
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 啟用 Edge-to-Edge。
        enableEdgeToEdge()

        // 建立 activity_main.xml 對應的 Binding 物件。
        binding = ActivityMainBinding.inflate(layoutInflater)

        // 將 Binding 的根 View 設為 Activity 畫面。
        setContentView(binding.root)

        // 處理狀態列及導覽列範圍。
        ViewCompat.setOnApplyWindowInsetsListener(
            binding.main
        ) { view, insets ->

            val systemBars =
                insets.getInsets(WindowInsetsCompat.Type.systemBars())

            view.setPadding(
                systemBars.left,
                systemBars.top,
                systemBars.right,
                systemBars.bottom
            )

            insets
        }

        // 設定第一個 TextView 的初始內容。
        binding.tv1.text = "1234"

        // 建立一個共用的圖片按鈕監聽器。
        // 重複使用同一個物件,可避免建立六個不同的監聽器物件。
        val imageButtonListener = MyImgButton()

        // 六個 ImageButton 全部使用相同的事件監聽器。
        binding.imageButton1.setOnClickListener(imageButtonListener)
        binding.imageButton2.setOnClickListener(imageButtonListener)
        binding.imageButton3.setOnClickListener(imageButtonListener)
        binding.imageButton4.setOnClickListener(imageButtonListener)
        binding.imageButton5.setOnClickListener(imageButtonListener)
        binding.imageButton6.setOnClickListener(imageButtonListener)
    }

    /**
     * 六個 ImageButton 共用的事件監聽器。
     *
     * 透過 inner class 存取 Activity 的 binding,
     * 再使用 when 判斷是哪個圖片按鈕被點擊。
     */
    inner class MyImgButton : View.OnClickListener {

        override fun onClick(view: View?) {

            when (view?.id) {

                // 點擊第一張圖片,修改 tv1。
                R.id.imageButton1 -> {
                    binding.tv1.text = "this is flower 1"
                }

                // 點擊第二張圖片,修改 tv2。
                R.id.imageButton2 -> {
                    binding.tv2.text = "this is flower 2"
                }

                // 點擊第三張圖片,修改 tv3。
                R.id.imageButton3 -> {
                    binding.tv3.text = "this is flower 3"
                }

                // 點擊第四張圖片,再次修改 tv1。
                R.id.imageButton4 -> {
                    binding.tv1.text = "this is flower 4"
                }

                // 點擊第五張圖片,再次修改 tv2。
                R.id.imageButton5 -> {
                    binding.tv2.text = "this is flower 5"
                }

                // 點擊第六張圖片,再次修改 tv3。
                R.id.imageButton6 -> {
                    binding.tv3.text = "this is flower 6"
                }
            }
        }
    }
}

參考資料:Android Developers-事件監聽器


一個監聽器物件重複使用

原始寫法為每個按鈕各建立一個 MyImgButton()

binding.imageButton1.setOnClickListener(MyImgButton())
binding.imageButton2.setOnClickListener(MyImgButton())
binding.imageButton3.setOnClickListener(MyImgButton())
binding.imageButton4.setOnClickListener(MyImgButton())
binding.imageButton5.setOnClickListener(MyImgButton())
binding.imageButton6.setOnClickListener(MyImgButton())

這樣可以正常執行,但會建立六個 MyImgButton 物件。

可以先建立一個物件,再讓六個按鈕共同使用:

val imageButtonListener = MyImgButton()

binding.imageButton1.setOnClickListener(imageButtonListener)
binding.imageButton2.setOnClickListener(imageButtonListener)
binding.imageButton3.setOnClickListener(imageButtonListener)
binding.imageButton4.setOnClickListener(imageButtonListener)
binding.imageButton5.setOnClickListener(imageButtonListener)
binding.imageButton6.setOnClickListener(imageButtonListener)

兩種寫法功能相同,但共用物件更能表達「這些按鈕由同一個監聽器統一管理」的設計。

參考資料:Android Developers-View.setOnClickListener


ImageView 與 ImageButton 有什麼不同?

比較項目 ImageView ImageButton
主要用途 顯示圖片 使用圖片作為按鈕
是否預期可以點擊 通常只顯示 預設用途就是互動
常見情境 商品圖、照片、封面 播放、刪除、上一張、下一張
圖片設定 android:src android:src
點擊事件 可以自行加入 通常會設定
無障礙說明 建議設定 必須特別注意

雖然 ImageView 也可以加入 setOnClickListener(),但如果元件的主要用途就是按鈕,使用 ImageButton 會更清楚。

圖片按鈕沒有文字標籤,因此 XML 中應加入 android:contentDescription,讓 TalkBack 等無障礙服務可以說明按鈕功能:

<ImageButton
    android:id="@+id/imageButton1"
    android:layout_width="80dp"
    android:layout_height="80dp"
    android:src="@drawable/img1"
    android:contentDescription="@string/show_flower_1" />

strings.xml

<resources>
    <string name="app_name">Widget Demo</string>
    <string name="show_flower_1">顯示第一張花朵圖片</string>
</resources>

參考資料:Android Developers-ImageButtonAndroid Developers-描述 UI 元素


四個範例的事件處理方式比較

範例 元件 取得元件方式 事件處理方式
範例一 Button、TextView View Binding 每個按鈕各自使用 Lambda
範例二 Button、TextView findViewById() 三個按鈕共用 MyButton
範例三 Button、ImageView findViewById() 每個按鈕各自使用 Lambda
範例四 ImageButton、TextView View Binding 六個按鈕共用 MyImgButton

如果每個按鈕的功能差異很大,而且程式碼不多,可以為每個按鈕分別撰寫 Lambda。如果有許多同類按鈕,且處理流程相似,則可以使用共用監聽器,再透過 when 判斷 View ID。

參考資料:Android Developers-回應點擊事件


findViewById 與 View Binding 再比較

今天的程式同時使用了兩種取得元件的方法。

findViewById

val imageView =
    findViewById<ImageView>(R.id.imageView)

優點是容易理解 XML ID 與 Kotlin 元件之間的關係,但每一個元件都需要手動取得。

View Binding

binding.imageButton1.setOnClickListener {
    // 處理事件
}

View Binding 會直接產生元件屬性,具有較好的型別安全與空值安全,也能減少重複程式碼。

Android 官方指出,在大多數使用情境中,View Binding 可以取代 findViewById()。對初學者而言,兩種方式都值得理解;建立新專案時,則可以優先使用 View Binding。

參考資料:Android Developers-View Binding 與 findViewById 的差異


今天學到的重點

今天透過四個實作範例,完成了 Android 常用 Widget 與點擊事件的練習:

  1. 使用 TextView 顯示與更新文字、數字。
  2. 使用 Button 接收使用者的點擊操作。
  3. 使用 ImageView 顯示 Drawable 圖片。
  4. 使用 setImageResource() 動態切換圖片。
  5. 使用 ImageButton 製作圖片式按鈕。
  6. 使用 findViewById() 取得 XML 元件。
  7. 使用 View Binding 簡化元件存取。
  8. 使用 Lambda 撰寫簡潔的點擊事件。
  9. 使用 View.OnClickListener 建立共用監聽器。
  10. 使用 when 依照 View ID 分流處理。
  11. 使用 inner class 存取 Activity 成員。
  12. 完成增加、減少與歸零的計數器。
  13. 為 ImageButton 加入無障礙說明。

這些元件看似簡單,卻是 Android App 人機互動的基礎。無論日後製作購物車數量調整、圖片瀏覽器、音樂播放器,或各種功能選單,都會使用到今天學習的事件處理觀念。

下一篇可以繼續認識 EditTextCheckBoxRadioButtonSwitchToast,讓 App 不只能接收點擊,也能取得使用者輸入及顯示操作結果。

參考資料:Android Developers-使用 Views 建構 UIAndroid Developers-輸入事件


上一篇
Android 畫面設計入門——ConstraintLayout、View Binding 與 Fragment 切換
下一篇
Android 點擊事件實戰——計數器、圖片切換與 ImageButton
系列文
Android 鐵人賽: 天命最高 - 陪伴大家一步步打造屬於自己的app13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言